先前在 Day 07 - gRPC 與 GraphQL 有介紹過,GraphQL 是一種查詢語言,用於 API,允許客戶端根據需求請求特定的數據,從而提升效率和靈活性。
在 Clean Architecture 中,GraphQL 屬於 Presentation 層,負責處理客戶端請求並與 Application 層交互,避免直接依賴資料存取。
GraphQL 與 gRPC 類似,都是最外部的 Presentation Layer,所以我們先在 src
資料夾下執行下方的 Scripts 來產生專案:
## Account GraphQL Project
cd .\Account\
dotnet new web -n Account.GraphQL
cd .\Account.GraphQL\
dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data
dotnet add .\Account.GraphQL.csproj reference ..\Account.Application\Account.Application.csproj
dotnet add .\Account.GraphQL.csproj reference ..\Account.Infrastructure\Account.Infrastructure.csproj
cd ..\..\
## Todo GraphQL Project
cd .\Todo\
dotnet new web -n Todo.GraphQL
cd .\Todo.GraphQL\
dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data
dotnet add .\Todo.GraphQL.csproj reference ..\Todo.Application\Todo.Application.csproj
dotnet add .\Todo.GraphQL.csproj reference ..\Todo.Infrastructure\Todo.Infrastructure.csproj
cd ..\..\
## Add Projects to Solution
dotnet sln add (ls -r **/*.csproj)
在這裡,我們使用 HotChocolate.AspNetCore 的套件來搭建 GraphQL 伺服器。這個套件提供了簡單易用的 API,讓我們能夠輕鬆建立 GraphQL 端點並處理查詢和資料篩選與變異。
由於 GraphQL 勢必會存取資料庫,故也需要 Reference Infrastructure Layer,以便透過資料訪問邏輯來取得所需的資料。
接下來,我們將設定 GraphQL 的基本結構,加入查詢(Query)和數據模型(Data Model),並將它們與 Application Layer 的 IRepository 進行整合。這樣一來,客戶端就能夠透過 GraphQL 發送請求,以獲取所需的數據,並同時保持應用的靈活性和效率。
我們先把基本的功能設置完成。
Query.cs
在 GraphQL 中,Query
是起頭的類型,我們可以在其中設定許多 Get Methods,用以定義客戶端可以發送的查詢。這樣的設計使得客戶端能夠靈活地獲取所需資料,並在一個請求中獲得所需的多個資料集。
namespace Account.GraphQL;
public class Query {}
修改 Program.cs
,這裡因為要使用到 Repository 來 Access Database,所以要 Add Application Layer 和 Infrastructure Layer,以便在 GraphQL 中使用這些層級的功能。
using Account.Application;
using Account.GraphQL;
using Account.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAccountApplication().AddAccountInfrastructure();
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddFiltering();
var app = builder.Build();
app.MapGraphQL();
app.Run();
最基本的設置到這裡就結束了,接下來我們只需要讓 Query
內有方法可以透過 Repository 拿到我們想要的資料,並轉譯成我們要的 DTO 即可。
下一步我們來讓 Repository 有回傳 Entity 的功能。首先是在 IUserRepository
加入 GetUsers
的方法,這裡回傳 IQueryable
的原因是因為它允許延遲查詢,這樣我們可以在查詢過程中動態添加過濾條件或排序,而不會立即執行查詢。這使得資料庫操作更加高效,並提供了更大的靈活性。
public interface IUserRepository : IRepository<User>
{
User? GetUserByEmail(string email);
void Add(User user);
IQueryable<User> GetUsers();
}
接著在 UserRepository
實作 GetUsers
,這裡回傳的 DbSet 也是 IQueryable 類型,可以保留 LINQ 查詢的能力,讓我們可以利用 EF Core 的功能進行更複雜的查詢操作。
public IQueryable<Domain.Aggregates.User> GetUsers()
{
return _accountContext.Users;
}
GetUsers
在 Query
內接下來,我們要在 Query 類中新增一個 GetUsers 方法,以便客戶端能夠通過 GraphQL 查詢使用者資料。我們將從 IUserRepository 獲取 IQueryable,然後將其轉換為 DTO,以滿足 GraphQL 的要求。以下是方法的實作範例:
using Account.Application;
using Account.Domain.ValueObjects;
using Account.GraphQL.Models;
namespace Account.GraphQL;
public class Query
{
[UseFiltering]
public IQueryable<UserDto> GetUsers([Service] IUserRepository repository)
=> repository.GetUsers()
.Select(user => new UserDto
{
Id = user.Id.Value.ToString(),
FirstName = user.FirstName,
LastName = user.LastName,
Email = user.Email,
CreatedDateTime = user.CreatedDateTime,
UpdatedDateTime = user.UpdatedDateTime
});
}
在這個方法中,我們將每個 User 實體轉換為 UserDto,這是一個用於 GraphQL 的數據傳輸物件,僅包含必要的字段,從而確保客戶端獲取到的資料精簡且高效。這樣一來,我們就能利用 GraphQL 提供的強大查詢功能,實現靈活的數據訪問。此外,[UseFiltering]
這個 Attribute 是 HotChocolate.Data 套件提供的自動產生 Filter 功能,可以使我們在 Query 時輕鬆 Filter 出我們想要的結果。
然後再完成 DTO 的類別,先產生 Models
資料夾,在其中 Create UserDto.cs
如下:
namespace Account.GraphQL.Models;
public class UserDto
{
public string? Id { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public DateTime? CreatedDateTime { get; set; }
public DateTime? UpdatedDateTime { get; set; }
}
另外我們也需要客製化一個 By UserId 拿回實體資料。
public UserDto? GetUserById([Service] IUserRepository repository, string userId)
=> repository.GetUsers()
.Where(user => user.Id == UserId.Create(new Guid(userId)))
.Select(user => new UserDto
{
Id = user.Id.Value.ToString(),
FirstName = user.FirstName,
LastName = user.LastName,
Email = user.Email,
CreatedDateTime = user.CreatedDateTime,
UpdatedDateTime = user.UpdatedDateTime
}).FirstOrDefault();
這樣就完成了,超級簡單。
先執行專案後到 http://localhost:[port]/graphql/
就可以訪問 HotChocolate 幫我們設計好的 GraphQL 工具。
我們 Create Document 後到 Schema Definition Tab 內看到我們創建的 Query 和 UserDto,其他的 Input 是套用 [UseFiltering]
後自動產生的 Filter 物件。
我們做一下簡單的查詢。
在查詢內加入 Filter 找到我們的目標資料,要注意因為我們特別處理 DbContext Configuration 的關係,這裡 ValueObject 無法被 Where 條件解析。
甚至可以在將條件參數化,並寫在 Variables 內。
另外測試一下 GetuserById,會另外做這一個功能是因為 UserId 這個 ValueObject 被我們在 DbContext Configuration 中特別處理了,上面 Where 的 Filter Input 會使 SQL 語法出錯。
大功告成!
在這個教學系列中,一度想要把 GraphQL 的介紹拿掉,因為 GraphQL 的靈活與高彈性,很容易造成大量的 Queries 在同一秒內產生,理論上我拿先前做好的 Repository 來當作 GraphQL 的實作有點不明智,不只因為 Aggregate 的 ValueObject 會造成 Filtering 的困擾,還有濫用 GraphQL 會有高併發的情況出現,現有的作法沒辦法很好的應付這種情況。GraphQL 的水深遠沒有我所介紹的這麼淺顯,效能優化和合理的架構設計對 GraphQL 來說是至關重要的,這裡只介紹了基本的應用,帶領讀者入門,之後的修行還是得靠個人。
另外上面 Account GraphQL Service 做完了,下一步的 Todo GraphQL Service 就請各位讀者實作看看,我在下方直接貼結果。下一章節我們會把 GraphQL 納入 Gateway 中。
using Todo.Application;
using Todo.Domain.ValueObjects;
using Todo.GraphQL.Models;
namespace Todo.GraphQL.Queries;
public class Query {}
[ExtendObjectType(typeof(Query))]
public class TodoListQuery
{
[UseFiltering]
public IQueryable<TodoListDto> GetTodoLists([Service] ITodoListRepository todoListRepository, [Service] ITodoItemRepository todoItemRepository)
=> todoListRepository.GetTodoLists()
.Select(todoList => new TodoListDto
{
Id = todoList.Id.Value.ToString(),
Name = todoList.Name,
Description = todoList.Description,
Status = todoList.Status,
UserId = todoList.UserId.ToString(),
CreatedDateTime = todoList.CreatedDateTime,
UpdatedDateTime = todoList.UpdatedDateTime
});
public TodoListDto? GetTodoListById([Service] ITodoListRepository todoListRepository, string listId)
=> todoListRepository.GetTodoLists()
.Where(list => list.Id == TodoListId.Create(new Guid(listId)))
.Select(todoList => new TodoListDto
{
Id = todoList.Id.Value.ToString(),
Name = todoList.Name,
Description = todoList.Description,
Status = todoList.Status,
UserId = todoList.UserId.ToString(),
CreatedDateTime = todoList.CreatedDateTime,
UpdatedDateTime = todoList.UpdatedDateTime
}).FirstOrDefault();
public List<TodoListDto>? GetTodoListsByUserId([Service] ITodoListRepository todoListRepository, string userId)
=> todoListRepository.GetTodoLists()
.Where(list => list.UserId == new Guid(userId))
.Select(todoList => new TodoListDto
{
Id = todoList.Id.Value.ToString(),
Name = todoList.Name,
Description = todoList.Description,
Status = todoList.Status,
UserId = todoList.UserId.ToString(),
CreatedDateTime = todoList.CreatedDateTime,
UpdatedDateTime = todoList.UpdatedDateTime
}).ToList();
}
[ExtendObjectType(typeof(Query))]
public class TodoItemQuery
{
[UseFiltering]
public IQueryable<TodoItemDto> GetTodoItems([Service] ITodoItemRepository todoItemRepository)
=> todoItemRepository.GetTodoItems()
.Select(todoItem => new TodoItemDto
{
Id = todoItem.Id.Value.ToString(),
Content = todoItem.Content,
State = todoItem.Status.State,
Color = todoItem.Status.Color,
ListId = todoItem.ListId.ToString(),
CreatedDateTime = todoItem.CreatedDateTime,
UpdatedDateTime = todoItem.UpdatedDateTime
});
public TodoItemDto? GetTodoItemById([Service] ITodoItemRepository todoItemRepository, string itemId)
=> todoItemRepository.GetTodoItems()
.Where(item => item.Id == TodoItemId.Create(new Guid(itemId)))
.Select(todoItem => new TodoItemDto
{
Id = todoItem.Id.Value.ToString(),
Content = todoItem.Content,
State = todoItem.Status.State,
Color = todoItem.Status.Color,
ListId = todoItem.ListId.ToString(),
CreatedDateTime = todoItem.CreatedDateTime,
UpdatedDateTime = todoItem.UpdatedDateTime
}).FirstOrDefault();
public List<TodoItemDto>? GetTodoItemsByListId([Service] ITodoItemRepository todoItemRepository, string listId)
=> todoItemRepository.GetTodoItems()
.Where(item => item.ListId == new Guid(listId))
.Select(todoItem => new TodoItemDto
{
Id = todoItem.Id.Value.ToString(),
Content = todoItem.Content,
State = todoItem.Status.State,
Color = todoItem.Status.Color,
ListId = todoItem.ListId.ToString(),
CreatedDateTime = todoItem.CreatedDateTime,
UpdatedDateTime = todoItem.UpdatedDateTime
}).ToList();
}
using Todo.Domain.ValueObjects.Enums;
namespace Todo.GraphQL.Models;
public class TodoListDto
{
public string? Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public TodoListStatus? Status { get; set; }
public string? UserId { get; set; }
public DateTime? CreatedDateTime { get; set; }
public DateTime? UpdatedDateTime { get; set; }
}
using Todo.Domain.ValueObjects.Enums;
namespace Todo.GraphQL.Models;
public class TodoItemDto
{
public string? Id { get; set; }
public string? Content { get; set; }
public TodoItemState? State { get; set; }
public string? Color { get; set; }
public string? ListId { get; set; }
public DateTime? CreatedDateTime { get; set; }
public DateTime? UpdatedDateTime { get; set; }
}
using Todo.Application;
using Todo.Infrastructure;
using Todo.GraphQL.Queries;
using HotChocolate.Stitching;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTodoApplication().AddTodoInfrastructure();
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddTypeExtension<TodoListQuery>()
.AddTypeExtension<TodoItemQuery>()
.AddFiltering();
var app = builder.Build();
app.MapGraphQL();
app.Run();
public IQueryable<TodoList> GetTodoLists()
{
return _todoContext.TodoLists;
}
public IQueryable<TodoItem> GetTodoItems()
{
return _todoContext.TodoItems;
}